use gl::types::*;
use std::ffi::CString;
use std::collections::HashMap;

pub struct Shader {
    pub id: GLuint,
}

impl Shader {
    pub fn new(shader_type: GLenum, source_code_string: &CString) -> Result<Shader, String> {
        unsafe {
            let shader_id = gl::CreateShader(shader_type);
            {
                let err = gl::GetError();
                if err != gl::NO_ERROR {
                    return Err(format!("Open GL error {err} at CreateShader"));
                }
            }
            gl::ShaderSource(shader_id, 1, &source_code_string.as_ptr(), std::ptr::null());
            {
                let err = gl::GetError();
                if err != gl::NO_ERROR {
                    return Err(format!("Open GL error {err} at ShaderSource"));
                }
            }
            gl::CompileShader(shader_id);
            let mut success = gl::FALSE as GLint;
            gl::GetShaderiv(shader_id, gl::COMPILE_STATUS, &mut success);
            if success != gl::TRUE as GLint {
                let mut info_log = Vec::with_capacity(1024);
                let mut buffer_size:GLsizei = 0;
                gl::GetShaderInfoLog(shader_id, 1024, &mut buffer_size as *mut GLsizei, info_log.as_mut_ptr() as *mut GLchar);
                info_log.set_len(buffer_size as usize);
                Err(format!(
                    "Open GL error, Shader({}) Compile Error: {}", shader_type, std::str::from_utf8(&info_log).unwrap()
                ))
            }
            else {
                Ok(Shader{id: shader_id})
            }
        }
    }
}

impl Drop for Shader {
    fn drop(&mut self) {
        unsafe {
            gl::DeleteShader(self.id);
            while gl::NO_ERROR != gl::GetError() { }
        }
    }
}

pub struct ShaderProgram {
    pub id: GLuint,
    uniforms_names: HashMap<String,CString>,
}

impl ShaderProgram {
    pub fn new() -> Result<ShaderProgram, String> {
        unsafe {
            let program_id = gl::CreateProgram();
            let err = gl::GetError();
            if err != gl::NO_ERROR {
                Err(format!("Open GL error {err} at CreateProgram"))
            } else {
                Ok(ShaderProgram{id: program_id, uniforms_names: HashMap::new()})
            }
        }
    }

    pub fn attach_shader(&self, shader:&Shader) -> Result<&Self, String> {
        unsafe {
            gl::AttachShader(self.id, shader.id);
            let err = gl::GetError();
            if err != gl::NO_ERROR {
                Err(format!("Open GL error {err} at AttachShader"))
            }
            else {
                Ok(self)
            }
        }
    }

    pub fn link_program(&self) -> Result<&Self, String> {
        unsafe {
            gl::LinkProgram(self.id);
            let mut success = gl::FALSE as GLint;
            gl::GetProgramiv(self.id, gl::LINK_STATUS, &mut success);
            if success != gl::TRUE as GLint {
                let mut info_log = Vec::with_capacity(1024);
                let mut buffer_size:GLsizei = 0;
                gl::GetProgramInfoLog(self.id, 1024, &mut buffer_size as *mut GLsizei, info_log.as_mut_ptr() as *mut GLchar);
                info_log.set_len(buffer_size as usize);
                Err(format!(
                        "Open GL error, Shader Program Compile / Link Error: {}", std::str::from_utf8(&info_log).unwrap()
                ))
            }
            else {
                Ok(self)
            }
        }
    }

    pub fn add_uniform_name(&mut self, uniform_name: &str) -> &Self {
        self.uniforms_names.insert(uniform_name.to_string(), CString::new(uniform_name.as_bytes()).unwrap());
        return self;
    }

    pub fn get_uniform_location(&self, uniform_name:&str) -> Result<GLint, String> {
        unsafe {
            gl::UseProgram(self.id);
            {
                let err = gl::GetError();
                if err != gl::NO_ERROR {
                    return Err(format!("Open GL error {err} at UseProgram"));
                }
            }
            let uniform_name_cstr = match self.uniforms_names.get(uniform_name) {
                Some(cstr) => cstr,
                None => {return Err(format!("No such a name: {}", uniform_name));}
            };
            let location = gl::GetUniformLocation(self.id, uniform_name_cstr.as_ptr());
            {
                let err = gl::GetError();
                if err != gl::NO_ERROR {
                    return Err(format!("Open GL error {err} at UseProgram"));
                }
            }
            return Ok(location);
        }
    }
}

impl Drop for ShaderProgram {
    fn drop(&mut self) {
        unsafe {
            gl::DeleteProgram(self.id);
            while gl::NO_ERROR != gl::GetError() { }
        }
    }
}